package plugin

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"

	_ "gitee.com/opengauss/openGauss-connector-go-pq"
	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
)

var (
	logger                    = log.DefaultLogger
	driverNameOpengauss       = "opengauss"
	defaultLimit        int64 = 100000
)

var (
	_ = driverNameOpengauss
)

func NewDataSourceInstance(set backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
	var bcfg baseCfg
	if err := json.Unmarshal(set.JSONData, &bcfg); err != nil {
		return nil, err
	}
	var tlsDir = ID
	s := &Service{
		macroEng:   newDBMacroEngine(bcfg.Timescaledb),
		tlsManager: newTLSManager(logger, tlsDir),
		rowLimit:   defaultLimit,
	}
	s.im = datasource.NewInstanceManager(s.initDB)
	return s, nil
}

type Service struct {
	macroEng   SQLMacroEngine
	tlsManager tlsSettingsProvider
	im         instancemgmt.InstanceManager
	db         *sql.DB
	rowLimit   int64
}

func (s *Service) initDB(set backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
	logger.Debug("Creating openGauss query endpoint")
	logger.Debug("InitDB set: ", set.JSONData)
	var (
		err     error
		connstr string
	)
	connstr, err = s.generateConnectionString(set)
	if err != nil {
		return nil, err
	}
	logger.Debug("Connect info: ", connstr)
	s.db, err = sql.Open(driverNameOpengauss, connstr)
	if err != nil {
		return nil, err
	}
	if s.rowLimit == 0 {
		s.rowLimit = defaultLimit
	}
	if err = s.db.Ping(); err != nil {
		return nil, err
	}
	var bcfg baseCfg
	if err := json.Unmarshal(set.JSONData, &bcfg); err != nil {
		return nil, err
	}
	if bcfg.MaxIdleConns > 0 {
		s.db.SetMaxIdleConns(bcfg.MaxIdleConns)
	}
	if bcfg.MaxOpenConns > 0 {
		s.db.SetMaxOpenConns(bcfg.MaxOpenConns)
	}
	if bcfg.ConnMaxLifetime > 0 {
		s.db.SetConnMaxLifetime(time.Duration(bcfg.ConnMaxLifetime) * time.Second)
	}

	return s, nil
}

func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	response := backend.NewQueryDataResponse()
	if s.db == nil {
		set := req.PluginContext.DataSourceInstanceSettings
		if _, err := s.initDB(*set); err != nil {
			return nil, err
		}
	}

	// use async
	for _, q := range req.Queries {
		logger.Debug("RefID: ", q.RefID, "Query Json: ", q.JSON)
		var queryModel QueryModel
		if err := json.Unmarshal(q.JSON, &queryModel); err != nil {
			return nil, err
		}
		if queryModel.RawQuery == "" {
			continue
		}
		var timeRange backend.TimeRange
		sqlText, err := s.macroEng.Interpolate(&q, timeRange, queryModel.RawQuery)
		if err != nil {
			return nil, err
		}
		res := s.query(sqlText)
		// save the response in a hashmap
		// based on with RefID as identifier
		response.Responses[q.RefID] = res

	}
	return response, nil
}

func (s *Service) query(sqlText string) (resp backend.DataResponse) {
	logger.Debug("Query sql:", sqlText)
	var rows *sql.Rows
	rows, resp.Error = s.db.Query(sqlText)
	if resp.Error != nil {
		logger.Error("Query sql err:", resp.Error)
		return
	}
	// create data frame response
	/*
			example:
			{
		    "results": {
		        "A": {
		            "frames": [
		                {
		                    "schema": {
		                        "refId": "A",
		                        "meta": {
		                            "executedQueryString": "SELECT wal_filename FROM brm.brm_backup LIMIT 50 "
		                        },
		                        "fields": [
		                            {
		                                "name": "wal_filename",
		                                "type": "string",
		                                "typeInfo": {
		                                    "frame": "string",
		                                    "nullable": true
		                                }
		                            }
		                        ]
		                    },
		                    "data": {
		                        "values": [
		                            [
		                                "4232324f",
		                                "05jkfdjdkf",
		                                "",
		                                ""
		                            ]
		                        ]
		                    }
		                }
		            ]
		        }
		    }
		}
	*/
	// combine frame
	frame, err := sqlutil.FrameFromRows(rows, s.rowLimit, sqlutil.ToConverters(s.GetConverterList()...)...)
	if err != nil {
		resp.Error = err
		logger.Error("Query combine frame err:", resp.Error)
		return
	}
	resp.Frames = append(resp.Frames, frame)

	return resp
}

func (s *Service) generateConnectionString(set backend.DataSourceInstanceSettings) (string, error) {
	type connInfo struct {
		URL      string `json:"url"`
		Database string `json:"database"`
		User     string `json:"user"`
	}
	var ci connInfo
	if err := json.Unmarshal(set.JSONData, &ci); err != nil {
		return "", err
	}

	if set.URL == "" {
		set.URL = ci.URL
	}
	if set.Database == "" {
		set.Database = ci.Database
	}
	if set.User == "" {
		set.User = ci.User
	}

	var host string
	var port int
	if strings.HasPrefix(set.URL, "/") {
		host = set.URL
		logger.Debug("Generating connection string with Unix socket specifier", "socket", host)
	} else {
		index := strings.LastIndex(set.URL, ":")
		v6Index := strings.Index(set.URL, "]")
		sp := strings.SplitN(set.URL, ":", 2)
		logger.Debug("Generating connection string with network host", "sp", sp)
		host = sp[0]
		if v6Index == -1 {
			if len(sp) > 1 {
				var err error
				port, err = strconv.Atoi(sp[1])
				if err != nil {
					return "", fmt.Errorf("invalid port in host specifier %q: %w", sp[1], err)
				}

				logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port)
			} else {
				logger.Debug("Generating connection string with network host", "host", host)
			}
		} else {
			if index == v6Index+1 {
				host = set.URL[1 : index-1]
				var err error
				port, err = strconv.Atoi(set.URL[index+1:])
				if err != nil {
					return "", fmt.Errorf("invalid port in host specifier %q: %w", set.URL[index+1:], err)
				}

				logger.Debug("Generating ipv6 connection string with network host/port pair", "host", host, "port", port)
			} else {
				host = set.URL[1 : len(set.URL)-1]
				logger.Debug("Generating ipv6 connection string with network host", "host", host)
			}
		}
	}

	connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'",
		escape(set.User), escape(set.DecryptedSecureJSONData["password"]), escape(host), escape(set.Database))
	if port > 0 {
		connStr += fmt.Sprintf(" port=%d", port)
	}

	tlsSettings, err := s.tlsManager.getTLSSettings(set)
	if err != nil {
		return "", err
	}

	connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode))

	// Attach root certificate if provided
	if tlsSettings.RootCertFile != "" {
		logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile)
		connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile))
	}

	// Attach client certificate and key if both are provided
	if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" {
		logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile)
		connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile))
	} else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" {
		return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified")
	}

	logger.Debug("Generated openGauss connection string successfully")
	return connStr, nil
}

// CheckHealth handles health checks sent from Grafana to the plugin.
// The main use case for these health checks is the test button on the
// datasource configuration page which allows users to verify that
// a datasource is working as expected.
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
	var (
		status  = backend.HealthStatusOk
		message = "openGauss connect success."
	)

	_, err := s.initDB(*req.PluginContext.DataSourceInstanceSettings)
	if err != nil {
		status = backend.HealthStatusError
		message = err.Error()
	}

	return &backend.CheckHealthResult{
		Status:  status,
		Message: message,
	}, nil
}

// escape single quotes and backslashes in openGauss connection string parameters.
func escape(input string) string {
	return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`)
}

// covert database internal data type to grafana panel type.
func (s *Service) GetConverterList() []sqlutil.StringConverter {
	return []sqlutil.StringConverter{
		{
			Name:           "handle FLOAT4",
			InputScanKind:  reflect.Interface,
			InputTypeName:  "FLOAT4",
			ConversionFunc: func(in *string) (*string, error) { return in, nil },
			Replacer: &sqlutil.StringFieldReplacer{
				OutputFieldType: data.FieldTypeNullableFloat64,
				ReplaceFunc: func(in *string) (interface{}, error) {
					if in == nil {
						return nil, nil
					}
					v, err := strconv.ParseFloat(*in, 64)
					if err != nil {
						return nil, err
					}
					return &v, nil
				},
			},
		},
		{
			Name:           "handle FLOAT8",
			InputScanKind:  reflect.Interface,
			InputTypeName:  "FLOAT8",
			ConversionFunc: func(in *string) (*string, error) { return in, nil },
			Replacer: &sqlutil.StringFieldReplacer{
				OutputFieldType: data.FieldTypeNullableFloat64,
				ReplaceFunc: func(in *string) (interface{}, error) {
					if in == nil {
						return nil, nil
					}
					v, err := strconv.ParseFloat(*in, 64)
					if err != nil {
						return nil, err
					}
					return &v, nil
				},
			},
		},
		{
			Name:           "handle NUMERIC",
			InputScanKind:  reflect.Interface,
			InputTypeName:  "NUMERIC",
			ConversionFunc: func(in *string) (*string, error) { return in, nil },
			Replacer: &sqlutil.StringFieldReplacer{
				OutputFieldType: data.FieldTypeNullableFloat64,
				ReplaceFunc: func(in *string) (interface{}, error) {
					if in == nil {
						return nil, nil
					}
					v, err := strconv.ParseFloat(*in, 64)
					if err != nil {
						return nil, err
					}
					return &v, nil
				},
			},
		},
		{
			Name:           "handle INT2",
			InputScanKind:  reflect.Interface,
			InputTypeName:  "INT2",
			ConversionFunc: func(in *string) (*string, error) { return in, nil },
			Replacer: &sqlutil.StringFieldReplacer{
				OutputFieldType: data.FieldTypeNullableInt16,
				ReplaceFunc: func(in *string) (interface{}, error) {
					if in == nil {
						return nil, nil
					}
					i64, err := strconv.ParseInt(*in, 10, 16)
					if err != nil {
						return nil, err
					}
					v := int16(i64)
					return &v, nil
				},
			},
		},
	}
}
